Report for HCR 55 DV Working Group

Author
Affiliation

Rebecca Tsang, Moji Abolfazli, and Adam S. Cohen

Hawaii State Judiciary

Published

January 16, 2026

Introduction

This report presents an analysis of TRO violations (HRS 586-4), Order for Protection violations (HRS 586-11), and harassment by stalking charges (HRS 711-1106.4 and HRS 711-1106.5). The analysis focus on statewide data, disaggregated by circuit [under construction!], over a reporting period between January 1st, 2017 and December 31st, 2025. The following sections contain tables and graphs to explore different aspects of the data.

Code
library(readxl)
library(dplyr)
library(tidyr)
library(DT)

df1 <- read_excel("HCR_55_extract_2026-01-15_0954.xlsx")

# wrangle data ------------------------------------------------------------

## create charge code section categories ----
df2 <- df1 %>% 
  mutate(cyear = as.numeric(format(FILING_DATE, "%Y")),
         charge_code_section = case_when(grepl("586-4", CHARGE_CODE) ~ "586-4",
                                         grepl("586-11", CHARGE_CODE) ~ "586-11",
                                         grepl("711-1106.4", CHARGE_CODE) ~ "711-1106.4",
                                         grepl("711-1106.5", CHARGE_CODE) ~ "711-1106.5",
                                         .default = CHARGE_CODE
                                         ),
         charge_code_section = factor(charge_code_section, levels = c("586-4", "586-11", "711-1106.4", "711-1106.5"))) 

#check how many of each offense are charged on each case -> there are indeed cases that have some combo of TRO, OFP, and 711-1106
df3_cases_check <- df2 %>% 
  group_by(CASE_ID, PERSON_ID, CHARGE_NUMBER) %>% #if multiple rows for a given charge, select most recent one
  slice(1) %>% 
  group_by(CASE_ID, PERSON_ID) %>% 
  summarise(
    total_charges = n(),
    TRO_violation = sum(grepl("586-4", charge_code_section)),
    OFP_violation = sum(grepl("586-11", charge_code_section)),
    harassment_by_stalking = sum(grepl("711-1106", charge_code_section))
  ) %>% 
  ungroup() %>% 
  mutate(combo_case = (TRO_violation >= 1) + (OFP_violation >= 1) + (harassment_by_stalking >= 1) >= 2)

Section 1: Cases filed

Out of 6,581 cases filed statewide for TRO violations, OFP violations, or harassment by stalking during the reporting period, 28.7% had more than one charge.

Multiple TRO charges: 10.7%
Multiple OFP charges: 15.9%
Multiple harassment by stalking charges: 0.2%
Combination of TRO violations, OFP violations, and harassment by stalking charges: 1.9%

Because some cases had a combination of TRO violations, OFP violations, and harassment by stalking charges, they are double counted in the case count statistics below.

No cases in this data set had multiple defendants, so the number of cases equals the number of defendants. However, some defendants had multiple cases. An analysis of defendants with multiple cases is provided in section #.

Code
df3_cases <- df2 %>% 
  group_by(CASE_ID, PERSON_ID, charge_code_section) %>% #get one row per charge type per case (allows us to double count when case has more than one charge type)
  slice(1) %>% 
  ungroup() %>% 
  count(charge_code_section, cyear)

### table data
df3_cases_table <- df3_cases %>% 
  pivot_wider(names_from = cyear, values_from = n)


datatable(df3_cases_table, 
          options = list(pageLength = 10, 
                        scrollX = TRUE)
          )
Code
library(plotly)

# Your code to create plot here
p1 <- ggplot(df3_cases, aes(y = n, x = cyear, group = charge_code_section, color = charge_code_section,
                      text = paste("Cases filed:", n, "<br>",
                                   "Charge:", charge_code_section, "<br>",
                                                         "Year:", cyear))) +
    geom_line() +
    geom_point() +
    scale_colour_discrete(
      limits = c("586-11", "586-4", "711-1106.5", "711-1106.4") # this reorders the legend
    ) +
    theme_bw() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
    labs(
      x = NULL,      # remove "cyear" label
      y = "Count",    # rename "n" to "Count"
      color = "Charge Code"
    ) +
    ggtitle("Frequency of DV case filings between 2017-2025") +
    NULL

plot1 <- ggplotly(p1, tooltip = "text")

plot1

Section 2: Charges filed

The data below represents the data at the charge level, To be consistent with case filing data, the year reflects the year the case was initially filed, not when the charge was filed.

At the charge level, cases can have more than one charge both within and across offense categories. The number of charges is necessarily larger than the number of case filings because of this.

Code
## Charge code sections by year----
df3_charges <- df2 %>% 
  count(charge_code_section, cyear)

### table data
df3_charges_table <- df3_charges %>% 
  pivot_wider(names_from = cyear, values_from = n)

# Display with DT
datatable(df3_charges_table, 
          options = list(pageLength = 10, 
                        scrollX = TRUE))
Code
library(plotly)

p2 <- ggplot(df3_charges, aes(y = n, x = cyear, group = charge_code_section, color = charge_code_section,
                      text = paste("Charges filed:", n, "<br>",
                                   "Charge:", charge_code_section, "<br>",
                                                         "Year:", cyear)
                      )) +
    geom_line() +
    geom_point() +
    scale_colour_discrete(
      limits = c("586-11", "586-4", "711-1106.5", "711-1106.4") # this reorders the legend
    ) +
    theme_bw() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
    labs(
      x = NULL,      # remove "cyear" label
      y = "Count",    # rename "n" to "Count"
      color = "Charge Code"
    ) +
    ggtitle("Frequency of DV Charges between 2017-2025") +
    NULL

plot2 <- ggplotly(p2, tooltip = "text")

plot2

Section 3: Charge Dispositions

Warning: Disposition estimates are highly unreliable due to inconsistent, inaccurate, and incomplete data entry. These figures may conflict with past and future reports and should be interpreted with significant. The severity of data quality issues makes these statistics unsuitable as a basis for decision-making.

The data below represents the dispositions, if any, on the charges in cases filed during the reporting period. To be consistent with case filing data, the year reflects the year the case was initially filed, not when the charge was disposed.

To view only one charge code section or one disposition category, use the search box. For example, typing “586-11” will show only rows for violation of orders for protection.

Code
# Dispositions analysis----
df3_dispos <- df2 %>% 
  mutate(dispo_category = case_when(DISPOSITION_CODE %in% c('DLP','DPJ','DPJ48','DSC','DSM','DWO','DWO48','SA','STR') ~ 'Discharged/Dismissed',
                                    DISPOSITION_CODE %in% c('DNP','NPCF','NPQ','CWD') ~ 'Nolle Prosequi',
                                    DISPOSITION_CODE %in% c('DAG','DJ','DNC','DPA','GLP','GLT','GLTB','GLTW', 'GLTJ', 'GP','JFS','NCP','NFA','NP','NCP','NOA','OTH','JFS','NCP') ~ 'Conviction',
                                    DISPOSITION_CODE %in% c("SUS") ~ "Sustained", #ALDRO
                                    DISPOSITION_CODE %in% c('CCC','CFC') ~ 'Committed to another Court',
                                    DISPOSITION_CODE %in% c('ACQ','ACQB', 'ACQJ', 'AFM','AQI','AQRI', 'CCER', 'CDS','CLP','CR','NGT','RES','REV','TAD','CHV','MOO','MRG','TDC','RDC') ~ 'Other',
                                    is.na(DISPOSITION_CODE) | DISPOSITION_CODE %in% c('','  ') | is.null(DISPOSITION_CODE) ~ 'Pending',
                                    .default = "More Other")) %>% 
  count(charge_code_section, dispo_category, cyear)

### table data
df3_dispo_table <- df3_dispos %>% 
  pivot_wider(names_from = cyear, values_from = n)


datatable(df3_dispo_table, 
          options = list(pageLength = 10, 
                        scrollX = TRUE))
Code
library(plotly)

p3 <- ggplot(df3_dispos, aes(y = n, x = cyear, group = dispo_category, color = dispo_category,
                      text = paste("Charges disposed:", n, "<br>",
                                   "Charge:", charge_code_section, "<br>",
                                                         "Year:", cyear)
                      )) +
    geom_line() +
    geom_point() +
    # scale_colour_discrete(
    #   limits = c("586-11", "586-4", "711-1106.5", "711-1106.4") # this reorders the legend
    # ) +
    theme_bw() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
    labs(
      x = NULL,      # remove "cyear" label
      y = "Count",    # rename "n" to "Count"
      color = "Charge Code"
    ) +
    ggtitle("Frequency of DV Charge Dispositions between 2017-2025") +
  facet_wrap(~ charge_code_section) +
    NULL

plot3 <- ggplotly(p3, tooltip = "text")

plot3

Section 4: Rule 48 Dismissals

WARNING: Rule 48 data is highly unreliable due to inconsistent data entry.

Code
df3_rule48 <- df2 %>% 
  filter(DISPOSITION_CODE %in% c("DPJ48", "DWO48")) %>% 
  count(charge_code_section, cyear) 

df3_rule48_table <- df3_rule48 %>% 
  pivot_wider(names_from = cyear, values_from = n, names_sort = TRUE)

datatable(df3_rule48_table, 
          options = list(pageLength = 10, 
                        scrollX = TRUE))
Code
library(plotly)

p4 <- ggplot(df3_rule48, aes(y = n, x = cyear, group = charge_code_section, color = charge_code_section,
                      text = paste("Charges disposed:", n, "<br>",
                                   "Charge:", charge_code_section, "<br>",
                                                         "Year:", cyear)
                      )) +
    geom_line() +
    geom_point() +
    # scale_colour_discrete(
    #   limits = c("586-11", "586-4", "711-1106.5", "711-1106.4") # this reorders the legend
    # ) +
    theme_bw() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
    labs(
      x = NULL,      # remove "cyear" label
      y = "Count",    # rename "n" to "Count"
      color = "Charge Code"
    ) +
    ggtitle("Frequency of DV Charges dismissed under Rule 48 between 2017-2025") +
    NULL

plot4 <- ggplotly(p4, tooltip = "text")

plot4